package jmine.tec.utils.reflection.fast.asm;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import jmine.tec.utils.reflection.fast.FastConstructor;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

/**
 * "Compilador" para {@link FastConstructor}
 * 
 * @author takeshi
 */
public class FastConstructorCompiler extends AbstractClassGenerator implements Opcodes {

    private static final Method CONSTRUCTOR_METHOD;

    private static final Method DECLARING_CLASS_METHOD;

    private static final Method PARAM_TYPES_METHOD;

    static {
        try {
            CONSTRUCTOR_METHOD = CompiledConstructor.class.getDeclaredMethod("build", Object[].class);
            DECLARING_CLASS_METHOD = CompiledConstructor.class.getMethod("getDeclaringClass");
            PARAM_TYPES_METHOD = CompiledConstructor.class.getMethod("getParamTypes");
        } catch (Exception e) {
            throw new ExceptionInInitializerError(e);
        }
    }

    private final ConcurrentMap<Constructor<?>, CompiledConstructor> cache = new ConcurrentHashMap<Constructor<?>, CompiledConstructor>();

    /**
     * @param cl o {@link OpenClassLoader} a ser usado
     */
    public FastConstructorCompiler(OpenClassLoader cl) {
        super(cl);
    }

    /**
     * Compila e instancia um {@link CompiledConstructor} para o {@link FastConstructor} passado
     * 
     * @param fc {@link FastConstructor}
     * @return {@link CompiledConstructor}
     */
    public CompiledConstructor compileAndLoad(FastConstructor fc) {
        if (fc instanceof CompiledConstructor) {
            return (CompiledConstructor) fc;
        }
        final Constructor<?> ctor = fc.getConstructor();
        CompiledConstructor cached = this.cache.get(ctor);
        if (cached != null) {
            return cached;
        }
        synchronized (ctor) {
            cached = this.cache.get(ctor);
            if (cached != null) {
                return cached;
            }
            final Class<?> declaringClass = fc.getDeclaringClass();
            final Class<?>[] paramTypes = fc.getParamTypes();
            final CompiledConstructor compiled = this.compileAndLoad(declaringClass, paramTypes);
            this.cache.put(ctor, compiled);
            return compiled;
        }
    }

    /**
     * Compila e instancia um {@link CompiledConstructor}
     * 
     * @param declaringClass {@link Class}
     * @param paramTypes array de parametros
     * @return {@link CompiledConstructor}
     */
    private CompiledConstructor compileAndLoad(final Class<?> declaringClass, final Class<?>... paramTypes) {
        String clName =
                FastClassGenerator.generateName(JMINE_TEC_GENERATED + "constructor/" + AbstractClassGenerator.getClassName(declaringClass));
        ClassWriter writer = BytecodeHelper.createClassWithEmptyConstructor(clName, CompiledConstructor.class);
        this.emmitConstructorMethod(writer, declaringClass, paramTypes);

        this.emmitGetDeclaredTypeMethod(writer, declaringClass);

        this.emmitGetParameterTypesMethod(writer, paramTypes);

        writer.visitEnd();
        CompiledConstructor instance = this.loadClass(clName, writer.toByteArray(), CompiledConstructor.class);
        return instance;
    }

    /**
     * @param writer {@link ClassWriter}
     * @param declaringClass {@link Class}
     */
    private void emmitGetDeclaredTypeMethod(ClassWriter writer, Class<?> declaringClass) {
        MethodVisitor mv = BytecodeHelper.overwritePublicMethod(writer, DECLARING_CLASS_METHOD);
        mv.visitCode();
        mv.visitLdcInsn(Type.getType(declaringClass));
        mv.visitInsn(ARETURN);
        mv.visitMaxs(0, 0);
        mv.visitEnd();
    }

    /**
     * Emite o classarray
     * 
     * @param writer {@link ClassWriter}
     * @param params {@link Class}
     */
    private void emmitGetParameterTypesMethod(ClassWriter writer, Class<?>[] params) {
        MethodVisitor mv = BytecodeHelper.overwritePublicMethod(writer, PARAM_TYPES_METHOD);
        mv.visitCode();
        BytecodeHelper.pushClassArray(params, mv);
        // only the array should be on the stack so we return it
        mv.visitInsn(ARETURN);
        mv.visitMaxs(0, 0);
        mv.visitEnd();
    }

    /**
     * Emite os bytecodes
     * 
     * @param writer {@link ClassWriter}
     * @param declaringClass {@link Class}
     * @param paramTypes parameters
     */
    private void emmitConstructorMethod(ClassWriter writer, Class<?> declaringClass, Class<?>[] paramTypes) {
        MethodVisitor mv = BytecodeHelper.overwritePublicMethod(writer, CONSTRUCTOR_METHOD);
        mv.visitCode();
        mv.visitTypeInsn(NEW, Type.getInternalName(declaringClass));
        mv.visitInsn(DUP);
        BytecodeHelper.generateExpandArrayToTypes(mv, 1, paramTypes);
        mv.visitMethodInsn(INVOKESPECIAL, Type.getInternalName(declaringClass), "<init>", Type.getMethodDescriptor(Type.VOID_TYPE,
                BytecodeHelper.toTypeArray(paramTypes)));
        mv.visitInsn(ARETURN);
        mv.visitMaxs(0, 0);
        mv.visitEnd();
    }

}
